
Option Strict On : Option Explicit On : Option Compare Binary : Option Infer On

Imports System
Imports System.Collections.ObjectModel
Imports System.Text.RegularExpressions
Imports System.Windows.Forms.Utility
Imports System.Text


Namespace Code.Coloriser

    ''' <summary>
    ''' Interface that all code colorisers need to implement.
    ''' Provides the basis of providing code colorign information to the application via the
    ''' IColorTokenProvider.GetColorTokens method
    ''' </summary>
    ''' <remarks></remarks>
    Public MustInherit Class IColorTokenProvider


#Region "property"

        ''' <summary>
        ''' Must be overridden to provide a list of keywords defined in 
        ''' each language.
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks>Keywords must be separated with spaces.</remarks>
        Protected MustOverride ReadOnly Property Keywords() As String


        ''' <summary>
        ''' Can be overridden to provide a list of preprocessors defined in 
        ''' each language.
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks>Preprocessors must be separated with spaces.</remarks>
        Protected Overridable ReadOnly Property Preprocessors() As String
            Get
                Return ""
            End Get
        End Property


        ''' <summary>
        ''' Must be overridden to provide a regular expression string
        ''' to match strings literals. 
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Protected MustOverride ReadOnly Property StringOrCharRegexStr() As String


        ''' <summary>
        ''' Must be overridden to provide a regular expression string
        ''' to match comments. 
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Protected MustOverride ReadOnly Property CommentRegexStr() As String


        ''' <summary>
        ''' Determines if the language is case sensitive.
        ''' </summary>
        ''' <value>
        ''' <b>True</b> if the language is case sensitive;
        ''' <b>false</b> if the language is not case sensitive
        ''' The default value is true.
        ''' </value>
        ''' <returns></returns>
        ''' <remarks>
        ''' A case-insensitive language formatter must override this 
        ''' property to return false.
        ''' </remarks>
        Public Overridable ReadOnly Property CaseSensitive() As Boolean
            Get
                Return True
            End Get
        End Property

#End Region


#Region "regex"

        Protected cachedRegexes As New Dictionary(Of CodeElement, Regex)

        ''' <summary>
        ''' Generates regex for the given code element(s)
        ''' </summary>
        ''' <param name="codeElement"></param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Protected Function GenerateRegex(Optional ByVal codeElement As CodeElement = _
                                         CodeElement.NormalCode Or CodeElement.StringOrChar Or _
                                         CodeElement.Preprocessor Or CodeElement.Keyword Or _
                                         CodeElement.Comment) As Regex
            'check if it's cached
            If cachedRegexes.ContainsKey(codeElement) Then Return cachedRegexes(codeElement)

            Dim regStr As New StringBuilder

            'if comment is required
            If (codeElement And Code.CodeElement.Comment) <> 0 Then
                regStr.Append("(?<")
                'named group
                regStr.Append(Code.CodeElement.Comment.ToString())
                regStr.Append(">")
                regStr.Append(Me.CommentRegexStr)
                regStr.Append(")")
            End If

            If regStr.Length <> 0 Then regStr.Append("|")

            'if string is required
            If (codeElement And Code.CodeElement.StringOrChar) <> 0 Then
                regStr.Append("(?<")
                'named group
                regStr.Append(Code.CodeElement.StringOrChar.ToString())
                regStr.Append(">")
                regStr.Append(Me.StringOrCharRegexStr)
                regStr.Append(")")
            End If

            If regStr.Length <> 0 Then regStr.Append("|")

            'if preprocessor is required
            If (codeElement And Code.CodeElement.Preprocessor) <> 0 Then
                regStr.Append("(?<")
                'named group
                regStr.Append(Code.CodeElement.Preprocessor.ToString())
                regStr.Append(">")
                'produce the reg str
                If Not String.IsNullOrEmpty(Me.Preprocessors) Then
                    Dim prepRegstr As String = Regex.Replace( _
                    Regex.Replace(Me.Preprocessors, _
                                  "\w+|-\w+|#\w+|@@\w+|#(?:\\(?:s|w)(?:\*|\+)?\w+)+|@\\w\*+", _
                                  "(?<=^|\W)$0(?=\W)"), " +", "|")
                    regStr.Append(prepRegstr)
                End If
                regStr.Append(")")
            End If

            If regStr.Length <> 0 Then regStr.Append("|")

            'if keyword is required
            If (codeElement And Code.CodeElement.Keyword) <> 0 Then
                regStr.Append("(?<")
                'named group
                regStr.Append(Code.CodeElement.Keyword.ToString())
                regStr.Append(">")
                'produce the reg str
                If Not String.IsNullOrEmpty(Me.Keywords) Then
                    Dim keyRegstr As String = Regex.Replace( _
                    Regex.Replace(Me.Keywords, _
                                  "\w+|-\w+|#\w+|@@\w+|#(?:\\(?:s|w)(?:\*|\+)?\w+)+|@\\w\*+", _
                                  "(?<=^|\W)$0(?=\W)"), " +", "|")
                    regStr.Append(keyRegstr)
                End If
                regStr.Append(")")
            End If

            'build the Regex object based on the regStr
            Dim regOpt As RegexOptions = RegexOptions.Singleline
            If (Not Me.CaseSensitive) Then regOpt = regOpt Or RegexOptions.IgnoreCase
            Dim result As New Regex(regStr.ToString(), regOpt)

            'add it to cache
            cachedRegexes.Add(codeElement, result)
            Return result
        End Function


        ''' <summary>
        ''' Regex for all the code elements
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Overridable ReadOnly Property AllCodeElementRegex() As Regex
            Get
                Return GenerateRegex()
            End Get
        End Property


        Protected m_commentRegex As Regex = Nothing

        ''' <summary>
        ''' Regex for all the comments
        ''' </summary>
        ''' <value></value>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Overridable ReadOnly Property CommentRegex() As Regex
            Get
                Return GenerateRegex(CodeElement.Comment)
            End Get
        End Property

#End Region


        ''' <summary>
        ''' get color tokens from the given source code 
        ''' </summary>
        ''' <param name="sourceCode"></param>
        ''' <param name="offset"></param>
        ''' <param name="codeElement"></param>
        ''' <returns></returns>
        ''' <remarks></remarks>
        Public Overridable Function GetColorTokens(ByVal sourceCode As String, _
                                                   Optional ByVal offset As Int32 = 0, _
                                                   Optional ByVal codeElement As CodeElement = _
                                                   CodeElement.NormalCode Or CodeElement.StringOrChar Or _
                                                   CodeElement.Preprocessor Or CodeElement.Keyword Or _
                                                   CodeElement.Comment) _
                                                   As Collection(Of ColorToken)

            Dim tokens As New Collection(Of ColorToken)
            'early exit because of an invalid or empty sourceCode
            If String.IsNullOrEmpty(sourceCode) Then Return tokens

            Dim endPos As Int32 = offset + sourceCode.Length

            'get the regex for the code element
            Dim codeRegex As Regex = GenerateRegex(codeElement)
            Dim matchColl As MatchCollection = codeRegex.Matches(sourceCode)
            Dim currentGroup As Group
            For Each tokenMatch As Match In matchColl
                'check each group of the match
                'we are very sure that only one group can be of success in each match

                'check comment group
                If (codeElement And Code.CodeElement.Comment) <> 0 Then
                    currentGroup = tokenMatch.Groups(Code.CodeElement.Comment.ToString())
                    If currentGroup IsNot Nothing And currentGroup.Success Then
                        'add it as a ColorToken
                        tokens.Add(New ColorToken( _
                                   offset + currentGroup.Index, _
                                   currentGroup.Length, _
                                   Coloriser.CodeColor(Code.CodeElement.Comment)))
                        Continue For 'next loop
                    End If
                End If

                'check string or char group
                If (codeElement And Code.CodeElement.StringOrChar) <> 0 Then
                    currentGroup = tokenMatch.Groups(Code.CodeElement.StringOrChar.ToString())
                    If currentGroup IsNot Nothing And currentGroup.Success Then
                        'add it as a ColorToken
                        tokens.Add(New ColorToken( _
                                   offset + currentGroup.Index, _
                                   currentGroup.Length, _
                                   Coloriser.CodeColor(Code.CodeElement.StringOrChar)))
                        Continue For 'next loop
                    End If
                End If

                'check preprocessor group
                If (codeElement And Code.CodeElement.Preprocessor) <> 0 Then
                    currentGroup = tokenMatch.Groups(Code.CodeElement.Preprocessor.ToString())
                    If currentGroup IsNot Nothing And currentGroup.Success Then
                        'add it as a ColorToken
                        tokens.Add(New ColorToken( _
                                   offset + currentGroup.Index, _
                                   currentGroup.Length, _
                                   Coloriser.CodeColor(Code.CodeElement.Preprocessor)))
                        Continue For 'next loop
                    End If
                End If

                'check keyword group
                If (codeElement And Code.CodeElement.Keyword) <> 0 Then
                    currentGroup = tokenMatch.Groups(Code.CodeElement.Keyword.ToString())
                    If currentGroup IsNot Nothing And currentGroup.Success Then
                        'add it as a ColorToken
                        tokens.Add(New ColorToken( _
                                   offset + currentGroup.Index, _
                                   currentGroup.Length, _
                                   Coloriser.CodeColor(Code.CodeElement.Keyword)))
                        Continue For 'next loop
                    End If
                End If
            Next

            Return tokens
        End Function

    End Class

End Namespace
